package com.zz.springboot.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.zz.springboot.anno.ImportExcelMark;
import com.zz.springboot.util.CloneUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.support.AopUtils;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;

/**
 * 导表入库监听器
 *
 * @param <T> excel实体类泛型
 * @param <M> Manager泛型
 * @desc 这套监听器模板的通用点在于：
 * 1）需要手动在ManagerImpl中使用@ImportExcelMark注解标记【落库】方法
 * 2）手动传入excel实体类泛型、Manager泛型、Manager对象
 * 3）如果excel实体类和Manager层中落库方法的参数实体类需要为同一个类，若不为同一个类，则两个类中的属性名需要一致
 * 4）建议批量插入方法的返回值为Integer
 */
@Slf4j
public class ImportExcelListener<T, M> extends AnalysisEventListener<T> {

    private M beanManager;

    /**
     * 每隔5条存储数据库，实际使用中可以100条，然后清理list ，方便内存回收
     **/
    private static final int BATCH_COUNT = 10;

    /**
     * 缓存的数据
     **/
    private List<T> cacheList = new ArrayList<>(BATCH_COUNT);

    /**
     * @param beanManager
     * @desc 因为插入不同的表，最底层是在操作不同的实体类Bean的Manager层或Mapper层进行的，
     * 所以可通过构造函数来传递不同的Manager或Mapper对象
     */
    public ImportExcelListener(M beanManager) {
        this.beanManager = beanManager;
    }

    /**
     * 每读取一条表格数据，都会触发该方法
     *
     * @param data    一条数据
     * @param context
     */
    @Override
    public void invoke(T data, AnalysisContext context) {
        cacheList.add(data);
        // 达到BATCH_COUNT了，需要去存储一次数据库，防止数据几万条数据在内存，容易OOM
        if (cacheList.size() >= BATCH_COUNT) {
            saveData(cacheList);
            // 存储完成清理 list
            cacheList = new ArrayList<>(BATCH_COUNT);
        }

    }

    /**
     * 读完全表后会触发该方法，避免遗漏数据
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 这里也要保存数据，确保最后遗留的数据也存储到数据库
        saveData(cacheList);
        log.info("所有数据解析完成！");
    }

    /**
     * 往数据库中插入数据
     *
     * @param dataList
     */
    public void saveData(List<T> dataList) {
        /**如果beanManager中的方法已被aop代理，那么：
         *      此处使用beanManager.getClass().getMethods()方式拿到的是代理方法，而不是真实方法
         *      如果想获得真实方法，则可以使用AomenManager.class.getMethods()方式
         */
        Method[] methods = beanManager.getClass().getMethods();
        //Method[] methods = beanManager.getClass().getDeclaredMethods();
        //Method[] methods =AopUtils.getTargetClass(beanManager).getMethods();

        for (Method method : methods) {
            /**
             * 本项目中，如果使用aop全局输出日志，建议只代理到Service层，而不到Manager层。否则使用注解方式导入Excel的方式将失效
             */
            if (method.isAnnotationPresent(ImportExcelMark.class)) {
                log.info("方法{}已被标记", method.getName());
                try {
                    /** 情况1：存在实体类数据类型转换时 **/
                    //获得method方法参数中List的泛型
                    Class<?> dtoClazz = getListGenericClassInMethod(method);
                    //log.info("{}方法中参数List的泛型为{}", method.getName(), dtoClazz);
                    List dtoList = CloneUtils.batchClone(dataList, dtoClazz);
                    //log.info("转换后的集合结果为{}", dtoList);
                    Integer result = (Integer) method.invoke(beanManager, dtoList);

                    /** 情况2：不存在实体类数据类型转换时 **/
                    //Integer result = (Integer) method.invoke(beanManager, dataList);
                    if (result == 0) {
                        log.error("导入Excel表失败！");
                        throw new RuntimeException("导入Excel表失败！");
                    }
                    log.info("导入Excel表成功！");
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } finally {
                }
            }
        }
    }

    /**
     * 获得List<T>中泛型类对应的Class对象
     *
     * @param method
     * @return
     */
    public Class<?> getListGenericClassInMethod(Method method) {
        Class<?> aClass = null;
        try {
            Type[] genericParameterTypes = method.getGenericParameterTypes();
            ParameterizedType listType = (ParameterizedType) genericParameterTypes[0];  //java.util.List<com.zz.excel.dto.ExcelTwoDto>
            Type actualType = listType.getActualTypeArguments()[0];  //class com.zz.excel.dto.ExcelTwoDto
            String typeName = actualType.getTypeName();  //com.zz.excel.dto.ExcelTwoDto
            aClass = Class.forName(typeName);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return aClass;
    }
}
